MVI 架构:从双向绑定到单向数据流
何为 MVI?
Model: 与其他 MVX 中的 Model 不同的是,MVI 的 Model 主要指 UI状态(State)。当前界面展示的内容无非就是UI状态的一个快照:例如数据加载过程、控件位置等都是一种UI状态 View: 与其他 MVX 中的 View 一致,可能是一个 Activity、Fragment 或者任意UI承载单元。MVI 中的 View 通过订阅 Intent 的变化实现界面刷新 Intent: 此 Intent 不是 Activity 的 Intent ,用户的任何操作都被包装成 Intent 后发送给 Model 进行数据请求
单向数据流
用户操作以 Intent 的形式通知Model => Model
基于Intent更新State => View
接收到State变化刷新UI。数据永远在一个环形结构中单向流动,不能反向流动:
优点
UI的所有变化来自 State ,所以只需聚焦 State ,架构更简单、易于调试 数据单向流动,很容易对状态变化进行跟踪和回溯 state 实例都是不可变的,确保线程安全 UI只是反应 State 的变化,没有额外逻辑,可以被轻松替换或复用 缺点
所有的操作最终都会转换成 State ,所以当复杂页面的 State 容易膨胀 state 是不变的,每当 state 需要更新时都要创建新对象替代老对象,这会带来一定内存开销 有些事件类的UI变化不适合用 state 描述,例如弹出一个 toast 或者 snackbar
“talk is cheap, show me the code。”
我们通过一个Sample看一下如何快速搭建一个MVI架构的项目。
代码结构:
依赖库:
// Added Dependencies
implementation "androidx.recyclerview:recyclerview:1.1.0"
implementation 'android.arch.lifecycle:extensions:1.1.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'
implementation 'com.github.bumptech.glide:glide:4.11.0'
//retrofit
implementation 'com.squareup.retrofit2:retrofit:2.8.1'
implementation "com.squareup.retrofit2:converter-moshi:2.6.2"
//Coroutine
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6"
代码中使用以下API进行请求
https://reqres.in/api/users
将得到结果:
1. Model 层
1.1 User
定义User
的data class
package com.my.mvi.data.model
data class User(
@Json(name = "id")
val id: Int = 0,
@Json(name = "first_name")
val name: String = "",
@Json(name = "email")
val email: String = "",
@Json(name = "avator")
val avator: String = ""
)
1.2 ApiService
定义 ApiService,getUsers
方法进行数据请求
package com.my.mvi.data.api
interface ApiService {
@GET("users")
suspend fun getUsers(): List<User>
}
1.3 Retrofit
创建 Retrofit 实例
object RetrofitBuilder {
private const val BASE_URL = "https://reqres.in/api/user/1"
private fun getRetrofit() = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(MoshiConverterFactory.create())
.build()
val apiService: ApiService = getRetrofit().create(ApiService::class.java)
}
1.4 Repository
定义 Repository,封装API请求的具体实现
package com.my.mvi.data.repository
class MainRepository(private val apiService: ApiService) {
suspend fun getUsers() = apiService.getUsers()
}
2. UI 层
Model 定义完毕后,开始定义UI层,包括 View、ViewModel 以及 Intent 的定义
2.1 RecyclerView.Adapter
首先,需要一个 RecyclerView 来呈现列表结果,定义 MainAdapter 如下:
package com.my.mvi.ui.main.adapter
class MainAdapter(
private val users: ArrayList<User>
) : RecyclerView.Adapter<MainAdapter.DataViewHolder>() {
class DataViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(user: User) {
itemView.textViewUserName.text = user.name
itemView.textViewUserEmail.text = user.email
Glide.with(itemView.imageViewAvatar.context)
.load(user.avatar)
.into(itemView.imageViewAvatar)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
DataViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.item_layout, parent,
false
)
)
override fun getItemCount(): Int = users.size
override fun onBindViewHolder(holder: DataViewHolder, position: Int) =
holder.bind(users[position])
fun addData(list: List<User>) {
users.addAll(list)
}
}
item_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="60dp">
<ImageView
android:id="@+id/imageViewAvatar"
android:layout_width="60dp"
android:layout_height="0dp"
android:padding="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/textViewUserName"
style="@style/TextAppearance.AppCompat.Large"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/imageViewAvatar"
app:layout_constraintTop_toTopOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/textViewUserEmail"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/textViewUserName"
app:layout_constraintTop_toBottomOf="@+id/textViewUserName" />
</androidx.constraintlayout.widget.ConstraintLayout>
2.2 Intent
定义 Intent 用来包装用户 Action
package com.my.mvi.ui.main.intent
sealed class MainIntent {
object FetchUser : MainIntent()
}
2.3 State
定义UI层的 State 结构体
sealed class MainState {
object Idle : MainState()
object Loading : MainState()
data class Users(val user: List<User>) : MainState()
data class Error(val error: String?) : MainState()
}
2.4 ViewModel
ViewModel 是 MVI 的核心,存放和管理 State,同时接受 Intent 并进行数据请求
package com.my.mvi.ui.main.viewmodel
class MainViewModel(
private val repository: MainRepository
) : ViewModel() {
val userIntent = Channel<MainIntent>(Channel.UNLIMITED)
private val _state = MutableStateFlow<MainState>(MainState.Idle)
val state: StateFlow<MainState>
get() = _state
init {
handleIntent()
}
private fun handleIntent() {
viewModelScope.launch {
userIntent.consumeAsFlow().collect {
when (it) {
is MainIntent.FetchUser -> fetchUser()
}
}
}
}
private fun fetchUser() {
viewModelScope.launch {
_state.value = MainState.Loading
_state.value = try {
MainState.Users(repository.getUsers())
} catch (e: Exception) {
MainState.Error(e.localizedMessage)
}
}
}
}
我们在 handleIntent
中订阅 userIntent
并根据Action类型执行相应操作。本case中当出现 FetchUser
的Action时,调用 fetchUser
方法请求用户数据。用户数据返回后,会更新 State,MainActivity
订阅此 State 并刷新界面。
2.5 ViewModelFactory
构造 ViewModel 需要 Repository,所以通过 ViewModelFactory 注入必要的依赖
class ViewModelFactory(private val apiService: ApiService) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
return MainViewModel(MainRepository(apiService)) as T
}
throw IllegalArgumentException("Unknown class name")
}
}
2.6 定义MainActivity
package com.my.mvi.ui.main.view
class MainActivity : AppCompatActivity() {
private lateinit var mainViewModel: MainViewModel
private var adapter = MainAdapter(arrayListOf())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setupUI()
setupViewModel()
observeViewModel()
setupClicks()
}
private fun setupClicks() {
buttonFetchUser.setOnClickListener {
lifecycleScope.launch {
mainViewModel.userIntent.send(MainIntent.FetchUser)
}
}
}
private fun setupUI() {
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.run {
addItemDecoration(
DividerItemDecoration(
recyclerView.context,
(recyclerView.layoutManager as LinearLayoutManager).orientation
)
)
}
recyclerView.adapter = adapter
}
private fun setupViewModel() {
mainViewModel = ViewModelProviders.of(
this,
ViewModelFactory(
ApiHelperImpl(
RetrofitBuilder.apiService
)
)
).get(MainViewModel::class.java)
}
private fun observeViewModel() {
lifecycleScope.launch {
mainViewModel.state.collect {
when (it) {
is MainState.Idle -> {
}
is MainState.Loading -> {
buttonFetchUser.visibility = View.GONE
progressBar.visibility = View.VISIBLE
}
is MainState.Users -> {
progressBar.visibility = View.GONE
buttonFetchUser.visibility = View.GONE
renderList(it.user)
}
is MainState.Error -> {
progressBar.visibility = View.GONE
buttonFetchUser.visibility = View.VISIBLE
Toast.makeText(this@MainActivity, it.error, Toast.LENGTH_LONG).show()
}
}
}
}
}
private fun renderList(users: List<User>) {
recyclerView.visibility = View.VISIBLE
users.let { listOfUsers -> listOfUsers.let { adapter.addData(it) } }
adapter.notifyDataSetChanged()
}
}
MainActivity 中订阅 mainViewModel.state
,根据 State 处理各种UI显示和刷新。
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.main.view.MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/buttonFetchUser"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/fetch_user"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
如上,一个完整的 MVI 项目就完成了。
最后
MVI 在 MVVM 的基础上,规定了数据的单向流动和状态的不可变性,这类似于前端的 Redux 思想,非常适合 UI 展示类的场景。MVVM 也好,MVI 也好都不是架构的最终形态,世界上没有完美的架构,要根据项目情况选择适合的架构进行开发。
推荐阅读:
↓关注公众号↓ | ↓添加微信交流↓ |
---|---|